Skip to Content

什么是OAuth2?

核心思想:一个绝妙的比喻

想象一下这个场景:

你住进了一家酒店,有一张房卡。现在,你想让酒店的保洁服务在你外出时帮你打扫房间,但你肯定不想把你的房卡直接给保洁员。因为这张房卡不仅能开你的房门,可能还能进入健身房、餐厅消费等。你更不希望保洁员拿着你的卡去复制一张。

于是,你找到了酒店前台,对他们说:“请在下午2点到3点之间,授权保洁服务进入我的房间(仅限打扫)。”

前台验证了你的身份后,给了保洁服务一张临时的、只能开你房门、并且只在指定时间段有效的“工作卡”。保洁员用这张卡完成了工作,之后这张卡就失效了。他们自始至终没有接触到你那张功能强大的主房卡。

这个比喻完美地解释了 OAuth 2.0 的核心:

  • 你 (You)资源所有者 (Resource Owner),数据(房间)的拥有者。
  • 你的房卡 (Your Key Card):你的用户名和密码,最高权限凭证。
  • 保洁服务 (Cleaning Service)客户端应用 (Client Application),一个想要访问你部分数据的第三方应用。
  • 酒店前台 (Hotel Front Desk)授权服务器 (Authorization Server),负责验证你的身份,并根据你的许可,发放“临时工作卡”。
  • 你的房间 (Your Room)资源服务器 (Resource Server),存放着你的数据(例如,你的Google相册、微信好友列表)。
  • 临时工作卡 (Temporary Work Card)访问令牌 (Access Token),一个临时的、有特定权限(Scope)、有有效期的凭证。

OAuth 2.0 的本质就是“授权委托” (Delegated Authorization)。 它允许用户授权第三方应用访问他们存储在另外一个服务上的数据,但无需将自己的用户名和密码提供给该第三方应用。


OAuth 2.0 的四个关键角色

  1. 资源所有者 (Resource Owner)

    • 就是用户本人,是数据的所有者。
  2. 客户端 (Client)

    • 想要访问用户数据的第三方应用程序。例如,一个想要读取你Google相册来打印照片的网站。
  3. 授权服务器 (Authorization Server)

    • 整个流程的核心,负责:
      • 验证资源所有者(用户)的身份。
      • 获取资源所有者的授权(“你是否同意XX应用访问你的头像和昵称?”)。
      • 如果授权成功,向客户端发放访问令牌 (Access Token)
    • 例如:accounts.google.com, graph.facebook.com
  4. 资源服务器 (Resource Server)

    • 存储受保护资源的服务器。它会验证客户端出示的访问令牌,如果令牌有效且权限足够,就向客户端提供其请求的数据。
    • 例如:Google Photos API, GitHub API。

注意:在很多大型服务中(如Google、Facebook),授权服务器和资源服务器虽然是两个逻辑角色,但可能由同一家公司运营,甚至部署在同一组服务器上。


核心组件:令牌 (Tokens)

令牌是 OAuth 2.0 流程的“通行证”。

  1. 访问令牌 (Access Token)

    • 作用:客户端携带它去访问资源服务器,以获取数据。
    • 特点
      • 生命周期短:通常只有几分钟到几小时,以降低泄露风险。
      • 权限有限 (Scoped):令牌包含的权限范围(Scope)由用户授权决定。例如,一个令牌可能只能“读取头像”,不能“修改资料”。
      • 格式:通常是一个不透明的字符串,或者是一个 JWT (JSON Web Token)。JWT 的好处是它自身就包含了权限、过期时间等信息,资源服务器可以自行验证,而无需再去查询授权服务器。
  2. 刷新令牌 (Refresh Token)

    • 作用:当访问令牌过期后,客户端可以使用刷新令牌向授权服务器申请一个新的访问令牌,而无需用户再次登录授权
    • 特点
      • 生命周期长:可以是几天、几个月,甚至永久有效(除非用户撤销)。
      • 高度敏感:必须安全地存储在客户端。一旦泄露,攻击者就可以不断获取新的访问令牌。
      • 一次性或可重复使用:取决于授权服务器的策略。
  3. 授权码 (Authorization Code)

    • 这是一个临时的、一次性的中间凭证。它的存在是为了安全。客户端先用它换取访问令牌和刷新令牌,这个交换过程在安全的后端服务器之间进行,避免了敏感的令牌在不安全的浏览器环境(前端)中传递。

四种主要的授权流程 (Grant Types)

OAuth 2.0 是一个“框架”,它定义了多种获取访问令牌的方式,以适应不同的应用场景。

1. 授权码模式 (Authorization Code Grant) - 最常用、最安全

这是功能最完整、流程最严密的授权模式。

  • 适用场景:有后端服务器的 Web 应用(例如,一个Java/Python/Node.js网站)。

  • 流程图解:

    1. 用户请求授权:用户在客户端应用上点击“使用微信登录”。客户端将用户重定向到微信的授权服务器,并附带参数:client_id (客户端ID), redirect_uri (回调地址), scope (申请的权限), response_type=code
    2. 用户授权:用户在微信的页面上登录,并同意授权。
    3. 返回授权码:微信的授权服务器将用户重定向回客户端预设的 redirect_uri,并附上一个授权码 (code)
    4. 换取令牌:客户端的后端服务器收到授权码后,带上这个 codeclient_idclient_secret (客户端密钥),向微信的授权服务器发起请求,换取 Access TokenRefresh Token
    5. 访问资源:客户端后端使用 Access Token 向微信的资源服务器请求用户信息。
  • 优点:Access Token 和 Refresh Token 不会经过用户浏览器,只在服务器之间传递,非常安全。

2. 授权码模式 + PKCE (Proof Key for Code Exchange)

这是对授权码模式的增强,现在是移动应用和单页应用 (SPA) 的最佳实践

  • 解决的问题:在移动端等“公共客户端”中,无法安全地存储 client_secret。如果有恶意应用在手机上拦截了上一步中的授权码 code,它就可以冒充正常应用去换取令牌。
  • PKCE 流程
    1. (发起请求前)客户端生成一个随机字符串 code_verifier,并对其进行哈希运算得到 code_challenge
    2. (请求授权时)客户端将 code_challenge 和哈希算法一起发送给授权服务器。
    3. (换取令牌时)客户端在发送 code 的同时,也发送原始的 code_verifier
    4. 授权服务器收到后,用同样的哈希算法计算 code_verifier,与之前收到的 code_challenge 进行比对。如果一致,才发放令牌。
  • 优点:即使 code 被截获,由于攻击者没有 code_verifier,也无法换取令牌,有效防止了授权码拦截攻击。

3. 简化模式 (Implicit Grant) - 已不推荐

  • 适用场景:纯前端应用(没有后端服务器的 SPA)。
  • 流程:用户授权后,授权服务器直接将 Access Token 作为 URL 的一部分返回给客户端。
  • 缺点
    • 令牌直接暴露在浏览器中,不安全。
    • 不支持 Refresh Token,令牌过期后用户必须重新授权。
  • 现状:由于安全风险,已被 授权码+PKCE 模式取代。

4. 客户端凭证模式 (Client Credentials Grant)

  • 适用场景没有用户参与的场景,即应用以自己的名义访问资源,而不是代表某个用户。例如,一个后台服务需要调用另一个服务的API。
  • 流程:客户端直接使用自己的 client_idclient_secret 向授权服务器申请访问令牌。整个过程与用户无关。

5. 资源所有者密码凭证模式 (Resource Owner Password Credentials Grant) - 已不推荐

  • 适用场景:用户高度信任客户端应用,例如官方自己开发的应用。
  • 流程:客户端直接收集用户的用户名和密码,然后向授权服务器换取令牌。
  • 缺点:完全违背了 OAuth 的初衷(不暴露密码),风险极高。除非万不得已,否则绝不使用。

OAuth 2.0 vs OpenID Connect (OIDC) - 一个常见的混淆点

这是一个非常重要的区别:

  • OAuth 2.0 是一个授权 (Authorization) 框架。它解决的问题是“你能做什么?”(Can you access my photos?)。它的产物是 Access Token
  • OpenID Connect (OIDC) 是一个构建在 OAuth 2.0 之上的认证 (Authentication) 协议。它解决的问题是“你是谁?”(Are you John Doe?)。

当你看到“使用Google登录”时,这背后其实是 OIDC 在工作。流程是这样的:

  1. 整个流程和 OAuth 2.0 的授权码模式几乎一样。
  2. 但在请求时,scope 参数里会包含一个特殊的值 openid
  3. 最终,授权服务器除了返回 Access Token,还会返回一个 ID Token
  4. 这个 ID Token 是一个 JWT,其中包含了用户的身份信息(如用户ID、姓名、邮箱等)。客户端可以通过验证这个 ID Token 来确认用户的身份。

简单说:OIDC = OAuth 2.0 + ID Token。 OIDC 负责登录认证,OAuth 2.0 负责后续的 API 访问授权。


总结

特性描述
核心目的授权委托:让用户在不泄露密码的情况下,授权第三方应用访问其在另一服务上的资源。
关键角色资源所有者、客户端、授权服务器、资源服务器。
核心组件Access Token (访问令牌)、Refresh Token (刷新令牌)。
最佳实践- 后端 Web 应用:使用授权码模式 (Authorization Code Grant)
- 移动/单页应用 (SPA):使用授权码模式 + PKCE
- M2M (机器对机器):使用客户端凭证模式 (Client Credentials Grant)
安全基石- HTTPS/TLS:所有通信必须加密。
- 短生命周期的 Access Token
- 权限范围控制 (Scope)
- 安全的 redirect_uri 校验
与 OIDC 的关系OAuth 2.0 用于授权(你能做什么),OIDC 基于 OAuth 2.0 用于认证(你是谁)。

OAuth 2.0 因其灵活性和安全性,已成为现代 Web 和移动应用 API 授权的事实标准。理解它的工作原理对于任何 Web 开发者来说都至关重要。

如何集成OAuth2?

好的,我们来深入探讨一下如何在 Spring Security 中集成 OAuth 2.0。这是一个非常常见的需求,Spring Security 提供了强大且模块化的支持。

首先,最重要的一点是:你的应用程序在 OAuth 2.0 流程中扮演什么角色?

Spring Security 将其支持分为三个主要场景,这与我们之前讨论的 OAuth 2.0 角色相对应:

  1. OAuth 2.0 客户端 (Client):你的应用需要使用第三方服务(如 Google、GitHub、微信)进行登录,或者需要调用这些服务的 API。这是最常见的用例。
  2. OAuth 2.0 资源服务器 (Resource Server):你的应用是一个 API 服务,需要保护自己的资源。它要求来访的请求必须携带一个有效的访问令牌 (Access Token) 才能访问。
  3. OAuth 2.0 授权服务器 (Authorization Server):你的应用本身要成为一个 OAuth 2.0 提供方(像 Google 或 GitHub 那样),负责用户认证、颁发令牌。

重要提示:在过去,spring-security-oauth 这个项目同时处理这三个角色。但它现在已被弃用。现代 Spring Security 使用全新的、独立的模块来处理这些场景,配置更简单、更安全。

  • 客户端 -> spring-boot-starter-oauth2-client
  • 资源服务器 -> spring-boot-starter-oauth2-resource-server
  • 授权服务器 -> spring-authorization-server (一个独立的 Spring 社区项目)

下面我们分别详细说明前两种最常见场景的集成方法。


场景一:构建 OAuth 2.0 客户端 (Login with Google/GitHub)

目标:让用户通过外部身份提供商(IdP)登录我们的 Spring Boot 应用。

1. 添加依赖

在你的 pom.xml 中,确保有以下依赖:

<dependencies> <!-- 核心安全依赖 --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-security</artifactId> </dependency> <!-- OAuth2 客户端支持 --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-oauth2-client</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> </dependencies>

2. 配置 application.yml

这是配置的核心。你需要告诉 Spring Security 如何与外部提供商通信。以 Google 和 GitHub 为例:

spring: security: oauth2: client: # "registration" 下定义了所有支持的 OAuth2 提供商 registration: google: # "google" 是一个自定义的注册ID provider: google # Spring Boot 预设了常见提供商(google, github, facebook, okta),可省略大部分配置 client-id: YOUR_GOOGLE_CLIENT_ID client-secret: YOUR_GOOGLE_CLIENT_SECRET # scope 定义了你希望从 Google 获取哪些权限 scope: - openid - profile - email # 登录成功后,Google 会将用户重定向到这个地址,必须与你在Google Cloud Platform上配置的一致 # redirect-uri: "{baseUrl}/login/oauth2/code/{registrationId}" # 这是默认值,可以不写 github: # "github" 是另一个注册ID provider: github client-id: YOUR_GITHUB_CLIENT_ID client-secret: YOUR_GITHUB_CLIENT_SECRET scope: - read:user # "provider" 部分可以用来定义非预设的、自定义的 OAuth2 服务器信息 # 如果使用Spring Boot预设的提供商,此部分可以省略 provider: google: # 这些URL都是Spring Boot预设好的,通常不需要手动配置 authorization-uri: https://accounts.google.com/o/oauth2/v2/auth token-uri: https://www.googleapis.com/oauth2/v4/token user-info-uri: https://www.googleapis.com/oauth2/v3/userinfo user-name-attribute: sub # 自定义提供商的例子 # my-custom-provider: # authorization-uri: ... # token-uri: ... # user-info-uri: ...

3. 配置安全规则 (SecurityFilterChain)

在你的安全配置类中,启用 OAuth2 登录非常简单:

import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; import org.springframework.security.web.SecurityFilterChain; @Configuration @EnableWebSecurity public class SecurityConfig { @Bean public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { http .authorizeHttpRequests(authorize -> authorize .requestMatchers("/", "/login**", "/error").permitAll() // 允许访问首页和登录相关页面 .anyRequest().authenticated() // 其他所有请求都需要认证 ) // 关键配置:启用 OAuth2 登录功能 .oauth2Login(oauth2 -> { // 你可以在这里进行自定义配置,例如自定义登录页面、成功处理器等 // .loginPage("/login"); }); return http.build(); } }

发生了什么?

  • oauth2Login() 这个方法会自动配置一个 OAuth2LoginAuthenticationFilter
  • 当你访问受保护的页面时,它会自动重定向到 Spring Security 默认的登录选择页面 (/login),上面会显示你在 application.yml 中配置的提供商(例如“Google”、“GitHub”)。
  • 点击链接后,它会处理重定向到 Google/GitHub 的逻辑(授权码流程)。
  • 它还会处理回调请求( /login/oauth2/code/{registrationId}),用授权码换取访问令牌,并获取用户信息。
  • 最后,它会将用户信息包装成一个 OAuth2User 对象,并存入 SecurityContextHolder,完成登录。

4. 在 Controller 中获取用户信息

登录成功后,你可以很方便地在 Controller 中获取用户信息:

import org.springframework.security.core.annotation.AuthenticationPrincipal; import org.springframework.security.oauth2.core.user.OAuth2User; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RestController; @RestController public class UserController { @GetMapping("/user") public String getUser(@AuthenticationPrincipal OAuth2User oauth2User) { // OAuth2User 包含了从提供商获取的所有用户信息 String name = oauth2User.getAttribute("name"); String email = oauth2User.getAttribute("email"); return "Hello, " + name + "! Your email is " + email; } }

场景二:构建 OAuth 2.0 资源服务器 (Protected API)

目标:我们的应用是一个 REST API,需要验证客户端发来的 Bearer Token(通常是 JWT),只有合法的令牌才能访问受保护的端点。

1. 添加依赖

<dependencies> <!-- 核心安全依赖 --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-security</artifactId> </dependency> <!-- OAuth2 资源服务器支持 --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-oauth2-resource-server</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> </dependencies>

2. 配置 application.yml

你需要告诉资源服务器如何验证 JWT。最现代、最简单的方式是提供授权服务器的 issuer-uri

spring: security: oauth2: resourceserver: jwt: # 关键配置:JWT 的签发者 URI。 # Spring Security 会自动访问这个 URI 的 ".well-known/openid-configuration" 端点, # 找到 jwks_uri,并从中获取用于验证 JWT 签名的公钥。 # 例如,对于 Keycloak,它可能是 http://localhost:8080/realms/my-realm issuer-uri: https://your-authorization-server.com/auth/realms/your-realm # 或者,如果你的授权服务器不支持 OIDC Discovery,你可以直接指定公钥集地址: # jwk-set-uri: https://your-authorization-server.com/auth/realms/your-realm/protocol/openid-connect/certs

3. 配置安全规则 (SecurityFilterChain)

配置非常简洁,只需启用资源服务器支持即可。

import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; import org.springframework.security.web.SecurityFilterChain; @Configuration @EnableWebSecurity public class SecurityConfig { @Bean public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { http .authorizeHttpRequests(authorize -> authorize .requestMatchers("/api/public").permitAll() // 公开访问的端点 // 所有 /api/** 的请求都需要认证,并且需要拥有 "read_profile" 的 scope .requestMatchers("/api/user/**").hasAuthority("SCOPE_read_profile") .anyRequest().authenticated() ) // 关键配置:启用 OAuth2 资源服务器支持,并指定使用 JWT 进行验证 .oauth2ResourceServer(oauth2 -> oauth2.jwt()); return http.build(); } }

发生了什么?

  • oauth2ResourceServer(oauth2 -> oauth2.jwt()) 会配置一个 BearerTokenAuthenticationFilter
  • 这个过滤器会拦截所有请求,检查 Authorization 头是否存在 Bearer <token>
  • 如果存在,它会提取 JWT,并使用从 issuer-uri 获取的公钥来验证 JWT 的签名、过期时间 (exp)、签发者 (iss) 等信息。
  • 验证成功后,它会解析 JWT 中的 scopescp 声明,并将其转换为 Spring Security 的 GrantedAuthority(例如,"read_profile" 会被转换为 SCOPE_read_profile)。
  • 然后,你就可以使用 .hasAuthority()@PreAuthorize 等标准 Spring Security 注解来进行方法级别的权限控制。

4. 在 Controller 中访问令牌信息

import org.springframework.security.core.annotation.AuthenticationPrincipal; import org.springframework.security.oauth2.jwt.Jwt; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; @RestController @RequestMapping("/api/user") public class UserApiController { @GetMapping("/info") public String getUserInfo(@AuthenticationPrincipal Jwt jwt) { // Jwt 对象包含了令牌的所有声明 (claims) String userId = jwt.getSubject(); // 'sub' claim String issuer = jwt.getIssuer().toString(); // 访问自定义声明 String customClaim = jwt.getClaimAsString("custom_claim"); return "User ID: " + userId + " from issuer: " + issuer; } }

总结

角色目的关键依赖核心配置 (.yml)核心Java配置
OAuth2 客户端使用外部服务登录/授权spring-boot-starter-oauth2-clientspring.security.oauth2.client.registration (配置 client-id, secret, scope).oauth2Login()
OAuth2 资源服务器保护自己的API,验证令牌spring-boot-starter-oauth2-resource-serverspring.security.oauth2.resourceserver.jwt.issuer-uri (配置令牌签发者).oauth2ResourceServer(c -> c.jwt())
Last updated on